package jmine.tec.persist.schema.impl.elements;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.persistence.SequenceGenerator;

import jmine.tec.persist.schema.api.ProcessingSchemaElement;
import jmine.tec.persist.schema.api.SchemaElement;
import jmine.tec.persist.schema.api.SqlDialect;
import jmine.tec.utils.Tuple;
import jmine.tec.utils.reflection.AnnotationUtils;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Table;

/**
 * SchemaElement representando o schema em si.
 * 
 * @author lundberg
 */
public class Schema implements ProcessingSchemaElement<Configuration> {

    public static final Log LOG = LogFactory.getLog(Schema.class);

    private final SortedMap<String, TableElement> tables = new TreeMap<String, TableElement>();

    private final SortedMap<String, SequenceElement> sequences = new TreeMap<String, SequenceElement>();

    private String schema;

    private List<String> warnings = new ArrayList<String>();

    /**
     * Construtor
     * 
     * @param mappedClasses
     */
    public Schema() {
    }

    /**
     * Devolve o nome da tabela, colocando o schema na frente se necessario FIXME verificar schema e catalog no {@link Table}
     * 
     * @param table a {@link Table}
     * @param config {@link Configuration}
     * @return String
     */
    public static String getTableName(Table table, Configuration config) {
        return getEffectiveTableName(table.getName(), config);
    }

    /**
     * Devolve o nome da tabela, colocando o schema na frente se necessario
     * 
     * @param table String
     * @param configuration {@link Configuration}
     * @return String
     */
    public static String getEffectiveTableName(String table, Configuration configuration) {
        String schema = configuration.getProperty(Environment.DEFAULT_SCHEMA);
        if (schema != null) {
            return schema + "." + table;
        }
        return table;
    }

    /**
     * {@inheritDoc}
     */
    public List<String> createStatements(SqlDialect dialect) {
        List<String> statements = new LinkedList<String>();
        statements.addAll(dialect.renderElements(this.tables.values()));
        statements.addAll(dialect.renderElements(this.getForeignKeys()));
        statements.addAll(dialect.renderElements(this.sequences.values()));
        statements.addAll(dialect.renderElements(this.getCheckConstraints()));
        statements.addAll(dialect.renderElements(this.getIndexes()));

        List<SchemaElement> allElements = new LinkedList<SchemaElement>(this.tables.values());
        allElements.addAll(this.getForeignKeys());
        allElements.addAll(this.sequences.values());
        allElements.addAll(this.getCheckConstraints());
        allElements.addAll(this.getIndexes());

        return dialect.postProcessSchema(statements, allElements);
    }

    /**
     * Obtém uma lista com todas as chaves estrangeiras na ordem em que devem ser criados os statements.
     * 
     * @return lista de chaves estrangeiras
     */
    private List<ForeignKeyElement> getForeignKeys() {
        List<ForeignKeyElement> fks = new LinkedList<ForeignKeyElement>();
        for (TableElement table : this.tables.values()) {
            fks.addAll(table.getForeignKeys());
        }
        Collections.sort(fks);
        return fks;
    }

    /**
     * Obtém uma lista com todas as check constraints na ordem em que devem ser criados os statements
     * 
     * @return lista de check constraints
     */
    private List<CheckConstraintElement> getCheckConstraints() {
        List<CheckConstraintElement> checks = new LinkedList<CheckConstraintElement>();
        for (TableElement table : this.tables.values()) {
            checks.addAll(new TreeSet<CheckConstraintElement>(table.getCheckConstraints()));
        }
        Collections.sort(checks);
        return checks;
    }

    /**
     * Obtém a listagem dos índices na ordem em que devem ser criados os statements
     * 
     * @return lista de índices
     */
    private List<IndexElement> getIndexes() {
        List<IndexElement> indexes = new LinkedList<IndexElement>();
        for (TableElement table : this.tables.values()) {
            indexes.addAll(table.getIndexes());
        }
        Collections.sort(indexes);
        return indexes;
    }

    /**
     * {@inheritDoc}
     */
    public void process(Configuration configuration) {
        this.schema = configuration.getProperty(Environment.DEFAULT_SCHEMA);

        Iterator<?> it = configuration.getClassMappings();
        while (it.hasNext()) {
            this.processClassMetadata(configuration, (PersistentClass) it.next());
        }
        for (TableElement table : this.tables.values()) {
            table.processSecondPass();
        }
    }

    /**
     * Processa os metadados de uma classe mapeada pelo Hibernate
     * 
     * @param configuration configuration
     * @param metadata mapeamento
     */
    private void processClassMetadata(Configuration configuration, PersistentClass metadata) {
        Class<?> klass = metadata.getMappedClass();
        if (metadata.getTable().isPhysicalTable()) {
            this.findTable(getTableName(metadata.getTable(), configuration)).process(new TableProcessingBean(configuration, metadata));
            Tuple<Class<?>, SequenceGenerator> s = AnnotationUtils.findClassAndAnnotationWithAnnotation(klass, SequenceGenerator.class);
            if (s != null) {
                final SequenceElement sequence = this.findSequence(getEffectiveTableName(s.tail().sequenceName(), configuration));
                sequence.process(s.tail());
                sequence.setTableName(getTableName(metadata.getTable(), configuration));
            }
        }
    }

    /**
     * Adiciona uma nova tabela ao schema.
     * 
     * @param table table
     */
    public void addTable(TableElement table) {
        table.setSchema(this);
        this.tables.put(table.getName(), table);
        if (this.schema != null && table.getName().indexOf('.') < 0) {
            throw new IllegalStateException("missing schema on table: " + table.getName());
        }
    }

    /**
     * Adiciona uma nova sequence ao schema.
     * 
     * @param sequence sequence
     */
    public void addSequence(SequenceElement sequence) {
        this.sequences.put(sequence.getName(), sequence);
    }

    /**
     * Adiciona uma nova chave estrangeira ao schema.
     * 
     * @param foreignKey chave estrangeira
     */
    public void addForeignKey(ForeignKeyElement foreignKey) {
        this.findTable(foreignKey.getReferencing()).addForeignKey(foreignKey);
    }

    /**
     * Adiciona uma nova check constraint ao schema.
     * 
     * @param checkConstraint check constraint
     */
    public void addCheckConstraint(CheckConstraintElement checkConstraint) {
        this.findTable(checkConstraint.getTable()).addCheckConstraint(checkConstraint);
    }

    /**
     * Adiciona um novo índice ao schema.
     * 
     * @param index índice
     */
    public void addIndex(IndexElement index) {
        this.findTable(index.getTable()).addIndex(index);
    }

    /**
     * Adiciona um novo comentário ao schema.
     * 
     * @param comment comentário
     */
    public void addComment(CommentElement comment) {
        this.findTable(comment.getTable()).addComment(comment);
    }

    /**
     * Adiciona uma nova chave primária ao schema
     * 
     * @param primaryKey chave primária
     */
    public void addPrimaryKey(PrimaryKeyElement primaryKey) {
        this.findTable(primaryKey.getTableName()).setPrimaryKey(primaryKey);
    }

    /**
     * Adiciona uma nova chave alternativa (unique constraint) ao schema.
     * 
     * @param alternateKey chave alternativa
     */
    public void addAlternateKey(AlternateKeyElement alternateKey) {
        this.findTable(alternateKey.getTable()).addAlternateKey(alternateKey);
    }

    /**
     * Encontra a tabela com o nome dado
     * 
     * @param tableName nome da tabela desejada
     * @return TableElement
     */
    public TableElement findTable(String tableName) {
        if (this.schema != null && tableName.indexOf('.') < 0) {
            return this.findTable(this.schema + "." + tableName);
        }
        if (!this.tables.containsKey(tableName)) {
            throw new IllegalStateException(String.format("Tabela '%s' não encontrada.", tableName));
        } else {
            return this.tables.get(tableName);
        }
    }

    /**
     * Encontra a sequence com o nome dado.
     * 
     * @param sequenceName nome da sequencia desejada
     * @return SequenceElement
     */
    public SequenceElement findSequence(String sequenceName) {
        if (this.schema != null && sequenceName.indexOf('.') < 0) {
            return this.findSequence(this.schema + "." + sequenceName);
        }
        if (!this.sequences.containsKey(sequenceName)) {
            throw new IllegalStateException(String.format("Sequencia '%s' não encontrada.", sequenceName));
        } else {
            return this.sequences.get(sequenceName);
        }
    }

    /**
     * Remove a tabela com o nome dado
     * 
     * @param table nome da tabela que deve ser removida
     */
    public void removeTable(String table) {
        this.tables.remove(table);
    }

    /**
     * @return the tables
     */
    public SortedMap<String, TableElement> getTables() {
        return this.tables;
    }

    /**
     * @return the sequences
     */
    public SortedMap<String, SequenceElement> getSequences() {
        return this.sequences;
    }

    /**
     * {@inheritDoc}
     */
    public void warn(String warning) {
        Schema.LOG.error(warning);
        this.warnings.add(warning);
    }

    /**
     * @return a lista de warnings gerados
     */
    public List<String> getWarnings() {
        return this.warnings;
    }

}
